Enumerable
is a module in the Ruby standard library. Classes that include it have
to define an instance method called each
, which yields the elements of the collection
in succession. Once the iterator is defined and Enumerable
is mixed in, the class
now supports all sorts of collection-related behavior. Also, keep in mind, that
although Enumerable
adds common functionality to each of the collection classes
that includes it, each of them actually overrides some of the methods.
class MyArray
include Enumerable
def each
# yield elements of the collection
end
end
You can query which methods Enumerable
provides by sending the message
instance_methods
to it and passing the false
argument so you get only methods
defined in the module itself.
irb(main):001:0> Enumerable.instance_methods(false)
In order to understand Enumerable
at a deeper level, let’s write a few of its
methods while trying to cover as many operations as posible.
Methods for Querying
Some Enumerable methods return information about the collection other than the elements themselves. Some return either true or false if one or more element matches certain criteria.
#include?
Returns true
if the item yielded to the block is equal the argument passed, false
otherwise.
def include?(value)
self.each { |item| return true if item == value }
return false
end
# (1..5).include?(2)
# => true
This works just fine for Arrays
but if we were querying a Hash
, we would need to
adjust for the fact that when you iterate through a Hash
with each
it yields back
one key/value pair at a time (two-element arrays). The Hash#include?
method checks
for key inclusion
#any?
Returns true
if any element meets a specified criteria, false
otherwise.
def any?
return self.length >= 1 unless block_given?
self.each do |element|
return true if yield(element)
end
return false
end
# => (1..5).any? { |n| n > 3 }
# => true
#count
Returns the count of elements based on a criteria passed as argument or a block, if given.
def count(value=nil)
counter = 0
self.each do |element|
if value
counter += 1 if element == value
elsif block_given?
counter += 1 if yield(element)
else
counter += 1
end
end
warn("warning: given block not used") if value && block_given?
return counter
end
# (1..5).count { |value| value > 3 }
# => 2
#tally
Counts the ocurrences of each element. Returning a Hash
with the collection’s elements
as keys and corresponding counts as values.
def tally
tally = Hash.new(0)
self.each { |element| tally[element] += 1 }
return tally
end
# [1, 2, 2, 2, 3, 3, 4].tally
# => {1=>1, 2=>3, 3=>2, 4=>1}
Methods for Searching and Filtering
It’s common to want to filter a collection of objects based on a selection criteria. We’ll look at several facilities for filtering and searching collections. All of them expect a code block, where you difine your selection criteria (your tests for inclusion or exclusion).
#find
aliased as #detect
Returns the first element in the collection for which the code block returns true
. If no
code block is provided, it returns an instance of Enumerator
.
def find
return self.enum_for(__method__) unless block_given?
self.each { |element| return element if yield(element) }
return nil
end
alias :detect :find
# (1..5).find {|value| value > 2}
# => 3
#find_all
aliased as #select
Returns an Array
containing all the elements of the original collection that matched
the criteria in the code block. If no matching elements are found, it returns an empty
collection. Also, if no code block is provided, it returns an instance of Enumerator
.
def find_all
return self.enum_for(__method__) unless block_given?
matches = []
self.each do |element|
matches << element if yield(element)
end
return matches
end
alias :select :find_all
# (1..9).find_all {|value| value % 3 == 0 }
# => [3, 6, 9]
#find_index
Returns the index of the first element that meets the specified criteria, or nil
if
no element matches the criteria. It returns an instance of Enumerator
if neither a code
block nor an argument are provided.
For this method to work, make sure your #each
implementation returns an
Enumerator
when no code block is provided. This will allow us to chain methods, as
in .each.with_index
.
def find_index(value=nil)
return self.enum_for(__method__) unless value || block_given?
self.each.with_index do |element, index|
if value
return index if element == value
else
return index if yield(element)
end
end
return nil
end
# (1..5).find_index(2)
# => 1
#group_by
When given a code block, it returns a Hash
. For each unique value returned by the
block, the results hash gets a key; the value for that key is an Array
of all the
elements of the collection for which the block returned that value. If no code block is
given, it returns an instance of Enumerator
.
def group_by
return self.enum_for(__method__) unless block_given?
groups = Hash.new([])
self.each do |element|
groups[yield(element)] << element
end
return groups
end
# colors = [{group: 'primary'}, {group: 'secondary'}, {group: 'primary'}]
# colors.group_by { |value| value[:group] }
# => {"primary"=>[{:group=>"primary"}, {:group=>"primary"}], "secondary"=>[{:group=>"secondary"}]}
#map
aliased as #collect
Returns an Array
when given a block. Returns and Enumerator
otherwise. The returned
array is always the same size as the original collection. Its elements are the result
of calling the code block on each element in the original object.
def map
return self.enum_for(__method__) unless block_given?
collection = []
self.each { |element| collection << yield(element) }
return collection
end
alias :collect :map
# (1..5).map { |value| value * value }
# => [1, 4, 9, 16, 25]
#inject
aliased as #reduce
Works by initializing an accumulator object, performs a calculation on each iteration and reset the accumulator to the result of that calculation. Returns the return value from the last block call.
def inject(operand=0)
accumulator = operand
self.each do |element|
accumulator = yield(accumulator, element)
end
return accumulator
end
alias :reduce :inject
# (1..5).inject { |sum, value| sum + value }
# => 15
We’ve gotten to know Enumerable
in a more detailed way and it will better
positioned us to handle everyday programming as we’ll use the enumeration-related
facilities of the language virtually every time we write a Ruby program.